import axios from 'axios';
import { PaginationResult } from '../objects/internal/PaginationResult';
import PaginationResponse from '../objects/inbound/PaginationResponse';
import { Database, randomString } from '../pouch/Database';
import { City, MapPoint } from '../pouch/models/City';
import { Staff, StaffForm } from '../pouch/models/Staff';
import { hashPassword, verifyPassword } from './Crypt';
import { Order } from '../pouch/models/Order';
import { getCenter } from './Map';
import { Role } from '../pouch/models/Role';
import { OrderState } from '../objects/internal/OrderState';

const db = new Database();

export default class Gopher {
	constructor() {
		db.migrate().catch(console.error);
	}

	public async checkToken(): Promise<boolean> {
		// mock check
		return false;
	}

	public async login(username: string, password: string): Promise<boolean> {
		// mock login, call actual login stuff with gopher here!
		const staffs = db.getCollection<Staff>('staffs');
		let account;
		try {
			account = await staffs.get(username);
		} catch (e) {
			console.error(e);
			console.log(`no such user: ${username}`);
			return false;
		}
		console.log(account);
		this.currentUsername = username;
		const out = verifyPassword(account.password, password, account.salt);
		if (out) {
			account.clockInTime = (new Date()).toISOString();
			await staffs.put(account);
		}
		return out;
	}

	public async logout(): Promise<void> {
		// mock logout, call actual logout stuff with gopher here!
	}

	public async listStaff(offset: number, limit: number): Promise<PaginationResult> {
		const collection = db.getCollection<Staff>('staffs');
		return Gopher.getPaginationResult(collection, offset, limit);
	}

	public async listStaffConverted(offset: number, limit: number): Promise<PaginationResult> {
		return this.listStaff(offset, limit).then(r => {
			return {
				total: r.total,
				data: r.data.map(x => {
					const staff = x as Staff;
					staff.registerTime = (new Date(staff.registerTime)).toLocaleString();
					staff.clockInTime = staff.clockInTime.length > 2 ? (new Date(staff.clockInTime)).toLocaleString() : staff.clockInTime;
					staff.role = this.getFriendlyRoleName(staff.role)
					return staff;
				}),
			};
		});
	}

	public async getStaff(key: string): Promise<Staff | undefined> {
		const collection = db.getCollection<Staff>('staffs');
		try {
			return await collection.get(key);
		} catch (ignored) {
			return undefined;
		}
	}

	public async addStaff(staff: StaffForm): Promise<boolean> {
		const collection = db.getCollection<Staff>('staffs');
		try {
			const salt = randomString();
			const password = await hashPassword(staff.password!, salt);
			const completeObject: Staff = {
				_id: staff.username,
				username: staff.username,
				displayName: staff.displayName,
				gender: staff.gender,
				password: password,
				salt: salt,
				role: staff.role,
				registerTime: (new Date()).toISOString(),
				clockInTime: '-',
				notes: staff.notes,
			};
			await collection.post(completeObject);
			return true;
		} catch (e) {
			return false;
		}
	}

	public async updateStaff(key: string, staff: StaffForm): Promise<boolean> {
		const collection = db.getCollection<Staff>('staffs');
		try {
			const originalDocument = await collection.get(key);
			if (staff.password) {
				const salt = randomString();
				staff.password = await hashPassword(staff.password, salt);
			}
			Object.assign(originalDocument, staff);
			await collection.put(originalDocument);
			return true;
		} catch (e) {
			console.error(e);
			return false;
		}
	}

	public async deleteStaff(key: string): Promise<void> {
		console.log(`Removing staff ${key}`);
		const collection = db.getCollection<Staff>('staffs');
		try {
			const staff = await collection.find({selector: {username: key}});
			if (staff.docs.length > 0) {
				await collection.remove(staff.docs[0]);
			}
		} catch (ignored) {
			console.error(ignored);
		}
	}

	public async listOrders(offset: number, limit: number): Promise<PaginationResult> {
		const collection = db.getCollection<Order>('orders');
		return Gopher.getPaginationResult(collection, offset, limit);
	}

	public async queryOrders(selector: object): Promise<Order[]> {
		const collection = db.getCollection<Order>('orders');
		try {
			const out = await collection.find({selector});
			return out.docs;
		} catch (ignored) {
			console.error(ignored);
			return [];
		}
	}

	public async forceCloseOrder(id: string) {
		const collection = db.getCollection<Order>('orders');
		try {
			const order = await collection.get(id);
			order.state = OrderState.CLOSED;
			await collection.put(order);
		} catch (e) {
			console.error(e);
		}
	}

	public async listOrdersConverted(offset: number, limit: number): Promise<PaginationResult> {
		return this.listOrders(offset, limit).then(r => {
			return {
				total: r.total,
				data: r.data.map(x => {
					const order = x as Order;
					Reflect.set(order, 'startTime', (new Date(order.startTime)).toLocaleString());
					Reflect.set(order, 'mileage', (order.mileage / 1000).toFixed(2));
					Reflect.set(order, 'duration', (order.duration / 60).toFixed(2));
					return order;
				})
			};
		});
	}

	public async listCities(): Promise<City[]> {
		const collection = db.getCollection<City>('cities');
		const result = await collection.allDocs();
		return Promise.all(result.rows.map(it => collection.get(it.key)));
	}

	public async getCity(city: string): Promise<City | undefined> {
		const collection = db.getCollection<City>('cities');
		try {
			const result = await collection.find({selector: {name: city}});
			return result.docs[0];
		} catch (e) {
			console.error(e);
			return undefined;
		}
	}

	public async getCentralPoint(city: string) {
		if (city === '全部') {
			return {
				lat: 25.317764,
				lng: 110.42232,
			};
		}
		const collection = db.getCollection<City>('cities');
		const result = await collection.find({selector: {name: city}});
		return getCenter(result.docs[0].region);
	}

	public async getKnownCities(): Promise<string[]> {
		const collection = db.getCollection<City>('cities');
		const cities = await collection.allDocs();
		return Promise.all(cities.rows.map(it => collection.get(it.key).then(x => x.name)));
	}

	public async updateContour(city: string, contour: MapPoint[]) {
		const collection = db.getCollection<City>('cities');
		try {
			const result = await collection.find({selector: {name: city}});
			if (result.docs.length > 0) {
				const originalDocument = result.docs[0];
				originalDocument.region = contour;
				await collection.put(originalDocument);
			} else {
				console.error(`city ${city} does not exist!`);
			}
		} catch (e) {
			console.error(e);
		}
	}

	public async addCity(city: string, contour: MapPoint[]) {
		const collection = db.getCollection<City>('cities');
		await collection.post({
			name: city,
			region: contour,
		});
	}

	public async deleteCity(city: string) {
		const collection = db.getCollection<City>('cities');
		try {
			const result = await collection.find({selector: {name: city}});
			if (result.docs.length > 0) {
				const originalDocument = result.docs[0];
				await collection.remove(originalDocument);
			}
		} catch (e) {
			console.error(e);
		}
	}

	public getCurrentUsername() {
		return this.currentUsername;
	}

	public getFriendlyRoleName(x: string): string {
		return this.friendlyRoleNames[x] || x;
	}

	public getFriendlyRoleNames(): Promise<Record<string, string>> {
		return this.friendlyRoleNamesPromise;
	}

	private async sendPaginationRequest<T>(endpoint: string, offset: number, limit: number) {
		const res = await this.gopher.get(`${endpoint}?offset=${offset}&limit=${limit}`);
		return res.data as PaginationResponse<T>;
	}

	private static async getPaginationResult<T extends object>(collection: PouchDB.Database<T>, offset: number, limit: number): Promise<PaginationResponse<T>> {
		const result = await collection.allDocs({skip: (offset - 1) * limit, limit: limit});
		const info = await collection.info();

		const data = await Promise.all(result.rows.map(it => collection.get(it.key)));
		const out = data.map(it => {
			let out: (T & { key: string }) = Object.create(null);
			Object.assign(out, it);
			Reflect.set(out, 'key', it!._id);
			return out;
		});

		console.log(out);

		return {
			code: 0,
			data: out,
			total: info.doc_count,
		};
	}

	private gopher = axios.create({
		baseURL: 'https://api.yourdomain.local/',
		timeout: 5000,
	});

	private currentUsername = 'TestAcct';
	private friendlyRoleNames: Record<string, string> = {};
	private friendlyRoleNamesPromise: Promise<Record<string, string>> = new Promise((resolve) => {
		const collection = db.getCollection<Role>('roles');
		collection.allDocs().then(keys => {
			Promise.all(keys.rows.map(key => collection.get(key.key))).then(xs => {
				xs.forEach(x => {
					this.friendlyRoleNames[x._id] = x.name;
				});
				resolve(this.friendlyRoleNames);
			});
		});
	});
}
